#ifndef VISUALTREATMENT_H
#define VISUALTREATMENT_H

/********************************************************************

created:	2024/02/03
updated:    2025/08/17
filename: 	visualtreatment.h
file base:	visualtreatment
file ext:	h
author:		HellRaven Mods

purpose:	Visualtreatment-Shader for NFS Carbon

*********************************************************************/

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CONSTANTS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#define FLASH_SCALE		    cvVisualTreatmentParams.x
#define COP_INTRO_SCALE     cvVisualTreatmentParams.z
#define BREAKER_INTENSITY   cvVisualTreatmentParams.y
#define VIGNETTE_SCALE		cvVisualTreatmentParams.w

shared float4x4 cmWorldView                         : cmWorldView; //WORLDVIEW
//shared float4x4	cmWorldViewProj                 : WorldViewProj;
shared float4x4 cmWorldMat                          : cmWorldMat; //local to world 

shared float3 cvLocalEyePos                         : cvLocalEyePos; //LOCALEYEPOS;
shared float4 cvBlurParams                          : cvBlurParams;
shared float4 cvTextureOffset                       : cvTextureOffset;
shared float4 cvVisualEffectFadeColour              : cvVisualEffectFadeColour;

static const int MAX_SAMPLES = 16;	// Maximum texture grabs

// Contains	sampling offsets used by the techniques 
shared float4 cavSampleOffsetWeights[MAX_SAMPLES]	: REG_cavSampleOffsetWeights;
shared float4 cavSampleOffsets[MAX_SAMPLES]         : REG_cavSampleOffsets;

// Tone	mapping	variables
shared float cfBloomScale                           : cfBloomScale;

shared float4 Coeffs0                               : CURVE_COEFFS_0;
shared float4 Coeffs1                               : CURVE_COEFFS_1;

shared float cfVignetteScale                        : cfVignetteScale;
shared float4 cvVisualTreatmentParams               : cvVisualTreatmentParams;
shared float4 cvVisualTreatmentParams2              : cvVisualTreatmentParams2;
shared float cfVisualEffectVignette                 : cfVisualEffectVignette;
shared float cfVisualEffectBrightness               : cfVisualEffectBrightness;

// Depth of Field variables
shared float4 cvDepthOfFieldParams : cvDepthOfFieldParams; //DEPTHOFFIELD_PARAMS;
shared bool cbDrawDepthOfField : cbDrawDepthOfField;
shared bool cbDepthOfFieldEnabled : cbDepthOfFieldEnabled;

// Sun direction (world-space, normalized) – injected by shaderloader
float4 cvSunDirection;
// Sun screen position (UV) – projected CPU-side by shaderloader
// .xy = screen UV, .z = 1.0 if visible (in front of camera), 0 if behind
float4 cvSunScreenPos;

// Auto-exposure: geometric-mean scene luminance injected by the shaderloader
// each frame via UpdateAutoExposure() + SetFloat("gAverageLuminance", ...).
// Default 0.18 = photographic middle-grey -> neutral fixed-exposure fallback.
float gAverageLuminance = 0.18;

// kExposureTarget is not user-facing: it is the luminance value we map to
// middle-grey. Changing it shifts the entire exposure curve uniformly, which
// is better left as an engineering constant rather than an artist slider.
static const float kExposureTarget = 0.18;

// Returns the per-frame exposure multiplier from scene luminance.
// ui_ExposureMin/Max are declared in visualtreatment.fx and injected by the
// shaderloader at runtime, giving the user full control over the clamp range.
// When ui_UseAutoExposure == 0 the function returns 1.0 (no adjustment).
float ComputeAutoExposure()
{
    if (ui_UseAutoExposure < 0.5) return 1.0;
    return clamp(kExposureTarget / max(gAverageLuminance, 0.0001),
                 ui_ExposureMin, ui_ExposureMax);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Samplers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
texture custom_tex1;
sampler CUSTOM_SAMPLER = sampler_state
{
    Texture = <custom_tex1>;
    MinFilter = Linear;
    MagFilter = Linear;
    MipFilter = Linear;
    AddressU = Clamp;
    AddressV = Clamp;
};

DECLARE_TEXTURE(DIFFUSEMAP_TEXTURE)
sampler	DIFFUSE_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(DIFFUSEMAP_TEXTURE)
    AddressU = CLAMP;
    AddressV = CLAMP;
    MIPFILTER = LINEAR;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(MOTIONBLUR_TEXTURE)
sampler	MOTIONBLUR_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(MOTIONBLUR_TEXTURE)
    AddressU = CLAMP;
    AddressV = CLAMP;
    MIPFILTER = NONE;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(DEPTHBUFFER_TEXTURE)
sampler	DEPTHBUFFER_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(DEPTHBUFFER_TEXTURE)
    AddressU = CLAMP;
    AddressV = CLAMP;
    MIPFILTER = NONE;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(VOLUMEMAP_TEXTURE)
sampler VOLUMEMAP_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(VOLUMEMAP_TEXTURE)
    AddressU = CLAMP;
    AddressV = CLAMP;
    AddressW = CLAMP;
    MIPFILTER = NONE;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(MISCMAP2_TEXTURE)
sampler	MISCMAP2_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(MISCMAP2_TEXTURE)
    AddressU = CLAMP;
    AddressV = WRAP;
    MIPFILTER = NONE;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(MISCMAP3_TEXTURE)
sampler BLOOM_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(MISCMAP3_TEXTURE)
    AddressU = CLAMP;
    AddressV = CLAMP;
    MIPFILTER = NONE;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(MISCMAP6_TEXTURE)			
sampler	MISCMAP6_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(MISCMAP6_TEXTURE)			
    AddressU = CLAMP;
    AddressV = CLAMP;
    MIPFILTER = LINEAR;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(BLENDVOLUMEMAP_TEXTURE)
sampler BLENDVOLUMEMAP_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(BLENDVOLUMEMAP_TEXTURE)
    AddressU = CLAMP;
    AddressV = CLAMP;
    AddressW = CLAMP;
    MIPFILTER = NONE;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Depth Sprite
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

struct DepthSpriteOut
{
float4 colour	: COLOR0;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Structs
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

struct VS_INPUT_SCREEN
{
    float4 position : POSITION;
    float4 tex0 : TEXCOORD0;
    float4 tex1 : TEXCOORD1;
};

struct VtoP
{
    float4 position : POSITION;
    float4 tex0 : TEXCOORD0;
    float4 tex1 : TEXCOORD1;
};

VtoP vertex_shader_passthru(const VS_INPUT_SCREEN IN)
{
    VtoP OUT;
    OUT.position = IN.position;
    OUT.tex0 = IN.tex0;
    OUT.tex1 = IN.tex1;

    return OUT;
}

float4 PS_PassThru(const VtoP IN) : COLOR
{
    float4 OUT;

    float4 diffuse = tex2D(DIFFUSE_SAMPLER, IN.tex0.xy);	// * IN;
    OUT.xyz = diffuse.xyz;
    OUT.w = diffuse.w;

    return OUT;
}

struct PS_INPUT
{
    float4 position : POSITION;
    float4 tex0 : TEXCOORD0;
    float4 tex1 : TEXCOORD1;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Texelsize + Time (drawn from CPU)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float2 texelSizeL0;
float2 texelSizeL1;
float2 texelSizeL2;
float2 texelSizeL3;
float2 texelSizeL4;
float Time;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Motionblur Quality Defines
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

static const int   SAMPLES = 16;
static const float jitterAmount = 0.0070;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Generate Noise Texture
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float2 Hash22(float2 p)
{
    // Dave Hoskins style hash, SM3-friendly (no trig)
    float3 p3 = frac(float3(p.xyx) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return frac((p3.xx + p3.yz) * p3.zy);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Generate HQ Jitter
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Motion Blur
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Motion Blur Vertex Shader
struct VS_INPUT_MOTIONBLUR
{
    float4 position : POSITION;
    float4 tex0 : TEXCOORD0;
    float4 tex1 : TEXCOORD1;
    float4 tex2 : TEXCOORD2;
    float4 tex3 : TEXCOORD3;
    float4 tex4 : TEXCOORD4;
    float4 tex5 : TEXCOORD5;
    float4 tex6 : TEXCOORD6;
    float4 tex7 : TEXCOORD7;
};

struct VtoP_MOTIONBLUR
{
    float4 position : POSITION;
    float2 tex[8] : TEXCOORD0;
};

// Vertex Shader
VtoP_MOTIONBLUR VS_MotionBlur(const VS_INPUT_MOTIONBLUR IN)
{
    VtoP_MOTIONBLUR OUT;
    OUT.position = IN.position;

    OUT.tex[0] = IN.tex0.xy;
    OUT.tex[1] = IN.tex1.xy;
    OUT.tex[2] = IN.tex2.xy;
    OUT.tex[3] = IN.tex3.xy;
    OUT.tex[4] = IN.tex4.xy;
    OUT.tex[5] = IN.tex5.xy;
    OUT.tex[6] = IN.tex6.xy;
    OUT.tex[7] = IN.tex7.xy;

    return OUT;
}

// Motion Blur Pixel Shader
// Xbox 360 / Carbon console-style radial blur, enhanced with HQ jitter.
//
// HOW IT WORKS
// Each of the 7 outer samples is interpolated between its original
// screen-space UV (from the vertex shader) and the current pixel center.
// ui_MotionBlurRadialPull controls how far each sample is pulled back:
//   0.5 = halfway to center (strong radial zoom),
//   1.0 = no pull at all (effectively disabled).
//
// JITTER
// A per-sample Hash22 jitter is added around each interpolated UV.
// This breaks the hard ring-shaped aliasing that appears at the blur
// boundary when driving in a straight line at high speed.
// Jitter scale is kept tight (jitterAmount / 4) so it adds noise
// without disrupting the radial character of the effect.
//
// MASK COMPATIBILITY
// base.w carries the depth value from MOTIONBLUR_SAMPLER (written by
// PS_DownScale2x2ForMotionBlur in screeneffect.fx).  PS_CompositeBlur
// reads this as blurIntensity and multiplies it by the vignette mask,
// so the full masking pipeline works identically to the old path.
float4 PS_MotionBlur(const VtoP_MOTIONBLUR IN) : COLOR
{
    float  pull = ui_MotionBlurRadialPull;
    float4 base = tex2D(MOTIONBLUR_SAMPLER, IN.tex[0]);

    // Per-sample jitter scale: tighter than the old HQ jitter to preserve
    // the radial feel while still breaking aliasing at blur edges.
    static const float kJitter = jitterAmount * 0.25;

    float3 s1 = tex2D(DIFFUSE_SAMPLER, saturate(lerp(IN.tex[1], IN.tex[0], pull) + Hash22(IN.tex[1] * 4096.0 + float2(17.0, 59.0)) * kJitter - kJitter * 0.5)).rgb;
    float3 s2 = tex2D(DIFFUSE_SAMPLER, saturate(lerp(IN.tex[2], IN.tex[0], pull) + Hash22(IN.tex[2] * 4096.0 + float2(34.0, 118.0)) * kJitter - kJitter * 0.5)).rgb;
    float3 s3 = tex2D(DIFFUSE_SAMPLER, saturate(lerp(IN.tex[3], IN.tex[0], pull) + Hash22(IN.tex[3] * 4096.0 + float2(51.0, 177.0)) * kJitter - kJitter * 0.5)).rgb;
    float3 s4 = tex2D(DIFFUSE_SAMPLER, saturate(lerp(IN.tex[4], IN.tex[0], pull) + Hash22(IN.tex[4] * 4096.0 + float2(68.0, 236.0)) * kJitter - kJitter * 0.5)).rgb;
    float3 s5 = tex2D(DIFFUSE_SAMPLER, saturate(lerp(IN.tex[5], IN.tex[0], pull) + Hash22(IN.tex[5] * 4096.0 + float2(85.0, 295.0)) * kJitter - kJitter * 0.5)).rgb;
    float3 s6 = tex2D(DIFFUSE_SAMPLER, saturate(lerp(IN.tex[6], IN.tex[0], pull) + Hash22(IN.tex[6] * 4096.0 + float2(102.0, 354.0)) * kJitter - kJitter * 0.5)).rgb;
    float3 s7 = tex2D(DIFFUSE_SAMPLER, saturate(lerp(IN.tex[7], IN.tex[0], pull) + Hash22(IN.tex[7] * 4096.0 + float2(119.0, 413.0)) * kJitter - kJitter * 0.5)).rgb;

    // 360 weighting: centre-heavy, outer samples fall off.
    // kB = 1/16 so all weights sum to exactly 1.0.
    const float kB = 1.0 / 16.0;
    float3 radial = base.rgb * (kB * 3.0)
                  + s1       * (kB * 3.0)
                  + s2       * (kB * 2.0)
                  + s3       * (kB * 2.0)
                  + s4       * (kB * 2.0)
                  + s5       * (kB * 1.5)
                  + s6       * (kB * 1.5)
                  + s7       * (kB * 1.0);

    return float4(radial, base.w);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Apply Motion Blur
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

VtoP VS_CompositeBlur(const VS_INPUT_SCREEN IN)
{
    VtoP OUT;

    OUT.position = IN.position;
    OUT.tex0 = IN.tex0;
    OUT.tex1 = IN.tex1;

    return OUT;
}

DepthSpriteOut PS_CompositeBlur(const VtoP IN)
{
    DepthSpriteOut OUT;

    float4 blur   = tex2D(MOTIONBLUR_SAMPLER, IN.tex0.xy);
    float4 screen = tex2D(DIFFUSE_SAMPLER,    IN.tex0.xy);

    // No vignette mask. The 360 radial blur reads the current frame
    // (DIFFUSE_SAMPLER) and pulls samples toward the pixel center, so
    // the effect is self-limiting by geometry -- no ghosting to contain,
    // no mask needed. The old mask only introduced a visible edge artifact.
    //
    // Blend is driven purely by blur.w (the depth-based intensity written
    // by PS_DownScale2x2ForMotionBlur) and cvBlurParams.x (engine speed
    // signal). Both go to zero at low speed, giving the ramp-up behaviour.
    float blurIntensity = saturate(blur.w * abs(cvBlurParams.x));

    float3 blurDiff = blur.rgb - screen.rgb;
    OUT.colour.rgb  = screen.rgb + blurDiff * blurIntensity;
    OUT.colour.a    = screen.a;

    return OUT;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Fake HDR function - ported from ReShade but optimized by me
// Not actual HDR - It just tries to mimic an HDR look
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 applyHDR(const VtoP IN, in float4 result)
{
    float HDRPower = ui_FakeHDRPower;

    // Two radii with a small gap between them – the HDR effect is driven
    // by the difference of the two blur averages.
    float radius1Px = ui_FakeHDRRadius;
    float radius2Px = ui_FakeHDRRadius * 0.95;

    float2 stepUV = texelSizeL0;

    float3 color = result.rgb;

    float3 bloom_sum1 = float3(0.0, 0.0, 0.0);
    float3 bloom_sum2 = float3(0.0, 0.0, 0.0);

    // Predefined unit circle directions to avoid cos/sin in the loop
    static const float2 directions[8] = {
        float2(1.0,  0.0), float2(0.707,  0.707),
        float2(0.0,  1.0), float2(-0.707, 0.707),
        float2(-1.0, 0.0), float2(-0.707,-0.707),
        float2(0.0, -1.0), float2(0.707,-0.707)
    };

    [loop]
        for (int i = 0; i < 8; ++i)
        {
            float2 dir = directions[i];

            float2 uv1 = IN.tex0.xy + dir * (radius1Px * stepUV);
            float2 uv2 = IN.tex0.xy + dir * (radius2Px * stepUV);

            // Safety clamp
            uv1 = saturate(uv1);
            uv2 = saturate(uv2);

            bloom_sum1 += tex2D(DIFFUSE_SAMPLER, uv1).rgb;
            bloom_sum2 += tex2D(DIFFUSE_SAMPLER, uv2).rgb;
        }

    bloom_sum1 *= (1.0 / 8.0);
    bloom_sum2 *= (1.0 / 8.0);

    float dist = radius2Px - radius1Px;
    float3 HDR = (color + (bloom_sum2 - bloom_sum1)) * dist;
    float3 blend = HDR + color;

    // Protect against zero and NaN
    float3 safeBlend = max(abs(blend), 0.0001);
    float3 hdrColor = blend * pow(safeBlend, HDRPower - 1.0) + HDR;

    return float4(saturate(hdrColor), result.w);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Vanilla Depth Of Field
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 DoDepthOfField(const VtoP IN, in float4 result, float depth)
{
    float zDist = 1.0 / max(1e-4, 1.0 - depth);
    float focalDist = cvDepthOfFieldParams.x;
    float depthOfField = cvDepthOfFieldParams.y;
    float falloff = cvDepthOfFieldParams.z;
    float maxBlur = cvDepthOfFieldParams.w;
    float blur = saturate((abs(zDist - focalDist) - depthOfField) * falloff / max(zDist, 1e-4));
    float mipLevel = blur * maxBlur;
    float3 blurredTex = tex2Dbias(MISCMAP6_SAMPLER, float4(IN.tex0.xy, 0, mipLevel)).rgb;
    result = float4(lerp(result.xyz, blurredTex, saturate(mipLevel)), result.w);

    if (cbDrawDepthOfField)
    {
        result.x += (1 - blur) * 0.3;
        result.xyz += (blur == 0.0f) ? 0.5 : 0.0f;
    }

    return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 24-Bit Depth Decode Utility
// Carbon packs depth across RGB channels (R=high 8, G=mid 8, B=low 8).
// This gives full 24-bit precision vs only ~8 bits from .r alone.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float readDepth24(float2 uv)
{
    float3 dc = tex2D(DEPTHBUFFER_SAMPLER, uv).rgb;
    return dot(dc, float3(65536.0 / 65793.0, 256.0 / 65793.0, 1.0 / 65793.0));
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Desaturation Shader
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 applyDesaturation(float4 result, float amount)
{
    float luminance = dot(result.rgb, LUMINANCE_VECTOR); // Calculate Luminance
    float3 desaturatedColor = lerp(result.rgb, float3(luminance, luminance, luminance), amount);
    return float4(desaturatedColor, result.a); // Keep Alphachannel
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Curve Shader
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Legacy Tone Mapping using Coeffs0-Coeffs3
float3 LegacyCurveTonemap(float3 color)
{
    float luminance = dot(color, LUMINANCE_VECTOR);

    // Ensure coefficients are valid, otherwise use fallback values
    float4 curve0 = Coeffs0;
    float4 curve1 = Coeffs1;

    if (all(curve0 == 0.0) || all(curve1 == 0.0)) {
        curve0 = float4(1.0, 1.0, 1.0, 0.0);  // Fallback to neutral curve
        curve1 = float4(0.5, 0.5, 0.5, 0.0);
    }

    // Compute curve normally
    float4 curve = curve1 * luminance + curve0;

    float3 adjustedColor = color * curve.xyz;

    // Debugging: Ensure colors never fully disappear
    // adjustedColor = max(adjustedColor, 0.15); // Minimum brightness level
    adjustedColor = lerp(color, adjustedColor, 0.6);

    return saturate(adjustedColor);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Color Grading & Split Toning + Contrast Adjustment
////////////////////////////////////////////////////////////////////////////////////////////////////////////////


float3 AdjustLumaChroma(float3 color, float lumaGain, float chromaGain)
{
    float luma = dot(color, LUMINANCE_VECTOR);
    float3 gray = float3(luma, luma, luma);
    float3 chroma = color - gray;

    color = gray * lumaGain + chroma * chromaGain;

    return color;
}

float3 AdjustContrast(float3 color, float contrast)
{
    return (color - 0.5) * contrast + 0.5;
}

// Piecewise linear curve through 4 evenly-spaced control points.
// Default identity: p0=0.0, p1=0.333, p2=0.667, p3=1.0
// Using plain lerp (not smoothstep) so default control points
// produce exact identity -- smoothstep would crush darks/lights.
float HermiteCurve(float x, float p0, float p1, float p2, float p3)
{
    float t; float a, b;
    if (x < 0.333)      { t = x / 0.333;           a = p0; b = p1; }
    else if (x < 0.667) { t = (x - 0.333) / 0.333; a = p1; b = p2; }
    else                 { t = (x - 0.667) / 0.333; a = p2; b = p3; }
    return lerp(a, b, t);
}

// Helper: hue in degrees [0,360] from RGB
float RGBtoHue(float3 c)
{
    float maxC = max(c.r, max(c.g, c.b));
    float minC = min(c.r, min(c.g, c.b));
    float delta = maxC - minC;
    if (delta < 1e-5) return 0.0;
    float h;
    if      (maxC == c.r) h = (c.g - c.b) / delta;
    else if (maxC == c.g) h = 2.0 + (c.b - c.r) / delta;
    else                  h = 4.0 + (c.r - c.g) / delta;
    h *= 60.0;
    if (h < 0.0) h += 360.0;
    return h;
}

// Helper: soft weight for a hue segment centred at hueCenter degrees
float HueWeight(float hue, float hueCenter, float hueRange)
{
    float diff = abs(hue - hueCenter);
    if (diff > 180.0) diff = 360.0 - diff;
    return smoothstep(hueRange, hueRange * 0.3, diff);
}

// Full color grading pipeline (DaVinci-style stage order)
float3 processColor(float3 color)
{
    // Stage 1: Channel Mixer
    float4 rowR, rowG, rowB;
    float  luma, chroma;
    rowR = ui_RedChannel; rowG = ui_GreenChannel; rowB = ui_BlueChannel;
    luma = ui_Luma; chroma = ui_Chroma;
    if (ui_PresetSelector > 0.5)
        GetDynamicPresetValues((int)ui_PresetSelector, rowR, rowG, rowB, luma, chroma);
    color = mul(color, float3x3(rowR.rgb, rowG.rgb, rowB.rgb));
    color = max(color, 0.0);

    // Stages 2-5: Advanced grading (only when ui_CGMode > 0.5)
    if (ui_CGMode > 0.5)
    {
        // Stage 2: Color Balance
        float lumW = dot(color, LUMINANCE_VECTOR);
        float shW  = 1.0 - smoothstep(0.0, 0.4, lumW);
        float hiW  = smoothstep(0.6, 1.0, lumW);
        float midW = max(1.0 - shW - hiW, 0.0);
        color += ui_CBShadows.rgb * shW + ui_CBMidtones.rgb * midW + ui_CBHighlights.rgb * hiW;
        color = max(color, 0.0);

        // Stage 3: Lift / Gamma / Gain
        float3 lift  = (ui_LGGLift.rgb  - 0.5) * 0.4;
        float3 gain  = max(ui_LGGGain.rgb  * 2.0, 0.001);
        float3 gamma = max(exp2((ui_LGGGamma.rgb - 0.5) * 3.0), 0.001);
        color = pow(max(color * gain + lift, 0.0), 1.0 / gamma);

        // Stage 4: RGB Curves
        color.r = HermiteCurve(color.r, ui_CurveMasterP0, ui_CurveMasterP1, ui_CurveMasterP2, ui_CurveMasterP3);
        color.g = HermiteCurve(color.g, ui_CurveMasterP0, ui_CurveMasterP1, ui_CurveMasterP2, ui_CurveMasterP3);
        color.b = HermiteCurve(color.b, ui_CurveMasterP0, ui_CurveMasterP1, ui_CurveMasterP2, ui_CurveMasterP3);
        color.r = HermiteCurve(color.r, ui_CurveRP0, ui_CurveRP1, ui_CurveRP2, ui_CurveRP3);
        color.g = HermiteCurve(color.g, ui_CurveGP0, ui_CurveGP1, ui_CurveGP2, ui_CurveGP3);
        color.b = HermiteCurve(color.b, ui_CurveBP0, ui_CurveBP1, ui_CurveBP2, ui_CurveBP3);

        // Stage 5: HSL per Segment
        float hue  = RGBtoHue(color);
        float grey = dot(color, LUMINANCE_VECTOR);
        float wR = HueWeight(hue,   0.0, 40.0);
        float wY = HueWeight(hue,  60.0, 40.0);
        float wG = HueWeight(hue, 120.0, 40.0);
        float wC = HueWeight(hue, 180.0, 40.0);
        float wB = HueWeight(hue, 240.0, 40.0);
        float wM = HueWeight(hue, 300.0, 40.0);
        float satScale = max((1.0 + ui_HSLRedSat*wR) * (1.0 + ui_HSLYellowSat*wY)
                           * (1.0 + ui_HSLGreenSat*wG)  * (1.0 + ui_HSLCyanSat*wC)
                           * (1.0 + ui_HSLBlueSat*wB)   * (1.0 + ui_HSLMagentaSat*wM), 0.0);
        color = lerp(grey.xxx, color, satScale);
        color += ui_HSLRedLum*wR + ui_HSLYellowLum*wY + ui_HSLGreenLum*wG
               + ui_HSLCyanLum*wC + ui_HSLBlueLum*wB  + ui_HSLMagentaLum*wM;
        color = max(color, 0.0);
    }

    // Stage 6: Luma / Chroma / Contrast (legacy, preset compat)
    color = AdjustLumaChroma(color, luma, chroma);
    color = AdjustContrast(color, ui_Contrast);

    return saturate(color);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Color Temperatur
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float3 adjustTemperature(float3 color, float temperature)
{
    // temperature in [-1, +1] ≈ small shifts
    float rScale = 1.0 + 0.10 * temperature;
    float bScale = 1.0 - 0.10 * temperature;

    float3 wb = float3(rScale, 1.0, bScale);

    // Normalize by perceived luminance so energy is preserved.
    // Without this, warm shifts brighten and cool shifts darken the image.
    wb *= rcp(dot(wb, LUMINANCE_VECTOR));

    return color * wb;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Sharpening Shader
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float LumaFromRGB(float3 c)
{
    // Prefer vector if present; otherwise use Rec.709-ish fallback.
#ifdef LUMINANCE_VECTOR
    return dot(c, LUMINANCE_VECTOR);
#else
    return dot(c, float3(0.2126, 0.7152, 0.0722));
#endif
}

float4 applySharpen(const VtoP IN, float4 result)
{
    float2 uv = IN.tex0.xy;

    // 1) Cross samples
    float3 c = result.rgb;
    float3 n = tex2D(DIFFUSE_SAMPLER, uv + float2(0.0, -texelSizeL0.y)).rgb;
    float3 s = tex2D(DIFFUSE_SAMPLER, uv + float2(0.0, texelSizeL0.y)).rgb;
    float3 w = tex2D(DIFFUSE_SAMPLER, uv + float2(-texelSizeL0.x, 0.0)).rgb;
    float3 e = tex2D(DIFFUSE_SAMPLER, uv + float2(texelSizeL0.x, 0.0)).rgb;

    // 2) CAS uses a contrast-adaptive gate derived from local min/max.
    // We'll compute it from luma to preserve hue.
    float lc = LumaFromRGB(c);
    float ln = LumaFromRGB(n);
    float ls = LumaFromRGB(s);
    float lw = LumaFromRGB(w);
    float le = LumaFromRGB(e);

    float mn = min(lc, min(ln, min(ls, min(lw, le))));
    float mx = max(lc, max(ln, max(ls, max(lw, le))));

    // 3) FidelityFX CAS-style "amplification" term.
    // amp grows when there is room before clipping and when contrast supports sharpening.
    float invMx = rcp(mx + 1e-5);
    float amp = saturate(min(mn, 2.0 - mx) * invMx);

    // 4) Strength mapping to CAS peak (negative weight).
    // ui_SharpenAmount expected in [0..1]. Higher => stronger sharpening.
    float a = saturate(ui_SharpenAmount);

    // CAS peak mapping: stronger peak as 'a' increases (matches common CAS tuning).
    // This keeps the denominator safely positive for the typical range.
    float peak = -rcp(lerp(8.0, 5.0, a));

    // Contrast-adaptive weight (negative), with sqrt shaping (CAS signature).
    float wLuma = peak * sqrt(amp);

    // 5) Apply convolution with negative neighbor weights.
    float denom = 1.0 + 4.0 * wLuma;

    // Safety: should already be > 0 with the peak mapping, but keep it bulletproof.
    denom = max(denom, 1e-3);

    float3 outColor = (c + (n + s + w + e) * wLuma) / denom;

    // CAS typically doesn't do an extra lerp here; the peak handles "amount".
    // Clamp to [0..1] at the end for post-processing safety in DX9.
    return float4(saturate(outColor), result.a);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Adaptive Whitebalance
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float3 adaptiveWhiteBalance(float3 color)
{
    float3 sceneColor = color;

    // Temperature proxy: Balance red against blue dominance
    float tempShift = dot(sceneColor.rgb, float3(1.0, 0.0, -1.0));

    // Apply automatic correction towards neutral white
    float3 correction = float3(-tempShift, 0.0, tempShift) * 0.1;
    color += correction;

    // Optional green/magenta shift correction
    float greenShift = sceneColor.g - ((sceneColor.r + sceneColor.b) * 0.5);
    color.rg += float2(0.01, -0.01) * greenShift;

    return color;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Chromatic Aberration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

static const float3 RGB_OFFSET = float3(1.0, 0.0, -1.0);

float4 applyChromaticAberration(const VtoP IN, in float4 result, float scale)
{
    float2 uv = IN.tex0.xy;
    float2 fromCenter = uv - 0.5;
    float dist = length(fromCenter);
    float2 dir = normalize(fromCenter + 1e-6);

    // Barrel distortion: quadratic falloff mimics real lens optics.
    // Linear CA looks artificial; real lenses bend exponentially at the edges.
    float barrelDist = dist * dist;

    // Per-channel radial offset
    float2 offsetR = uv + dir * scale * RGB_OFFSET.r * barrelDist;
    float2 offsetG = uv + dir * scale * RGB_OFFSET.g * barrelDist;
    float2 offsetB = uv + dir * scale * RGB_OFFSET.b * barrelDist;

    float r = tex2D(DIFFUSE_SAMPLER, offsetR).r;
    float g = tex2D(DIFFUSE_SAMPLER, offsetG).g;
    float b = tex2D(DIFFUSE_SAMPLER, offsetB).b;

    return float4(r, g, b, result.a);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Bloom Helpers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float BloomKnee(float x, float threshold, float softKnee)
{
    // UE-style soft-knee curve. Returns extracted energy (>=0).
    float knee = threshold * softKnee;
    float d = x - threshold;

    // Hard part
    float hard = max(d, 0.0);

    // Soft part
    float soft = max(d + knee, 0.0);
    soft = (knee > 1e-5) ? (soft * soft) / (4.0 * knee + 1e-5) : 0.0;

    return max(hard, soft);
}

float3 ApplyBloomThresholdSoftKnee(float3 bloomColor)
{
    float lum = dot(bloomColor, LUMINANCE_VECTOR);

    float extracted = BloomKnee(lum, ui_BloomThreshold, ui_BloomSoftKnee);

    // Convert extracted energy into a scale factor for RGB (preserves hue)
    float scale = extracted / max(lum, 1e-5);

    bloomColor *= scale;

    return bloomColor;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// LensDirt shader (BF3/Chapman method)
// The dirt texture modulates the bloom contribution – bright bloom areas
// shine through the dirt pattern. No threshold needed: the bloom buffer
// is already bright-passed by the engine's DownScaleForBloom technique.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 applyLensDirt(const VtoP IN, float4 baseColor)
{
    // 1. Fetch Dirt Texture (with optional chromatic shift)
    float3 dirtTex;
    if (ui_LensDirtChroma > 0.001) {
        float2 offset = float2(ui_LensDirtChroma, 0.0) * texelSizeL0 * 200.0;
        dirtTex.r = tex2D(CUSTOM_SAMPLER, IN.tex0.xy + offset).r;
        dirtTex.g = tex2D(CUSTOM_SAMPLER, IN.tex0.xy).g;
        dirtTex.b = tex2D(CUSTOM_SAMPLER, IN.tex0.xy - offset).b;
    }
    else {
        dirtTex = tex2D(CUSTOM_SAMPLER, IN.tex0.xy).rgb;
    }

    // 2. Bloom is the light source – already bright-passed by the engine.
    //    No additional threshold. Just read it.
    float3 bloom = tex2D(BLOOM_SAMPLER, IN.tex0.xy).rgb;

    // 3. Dirt = bloom modulated by dirt texture.
    //    This is the core BF3/Chapman insight: dirt acts as a per-pixel
    //    mask on the bloom layer, not a separate effect.
    float3 dirtLayer = bloom * dirtTex * ui_LensDirtIntensity;

    // 4. Edge emphasis (optional): stronger at screen edges
    float2 fromCenter = IN.tex0.xy - 0.5;
    float edgeDist = saturate(length(fromCenter) * 2.0);
    dirtLayer *= lerp(1.0, pow(edgeDist, ui_LensDirtEdgePower), step(0.01, ui_LensDirtEdgePower));

    // 5. Blend mode: 0=Additive, 1=Screen
    float3 additive = baseColor.rgb + dirtLayer;
    float3 screen = 1.0 - (1.0 - baseColor.rgb) * (1.0 - dirtLayer);

    return float4(lerp(additive, screen, ui_LensDirtBlendMode), baseColor.a);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Filmgrain shader
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 applyFilmGrain(const VtoP IN, float4 result)
{
    float2 noiseUV = IN.tex0.xy * ui_FilmGrainSize;
    float  t = frac(Time * 0.223);
    float  noise = rand2d(noiseUV + t); // rand2d aus global.h
    float  lum = dot(result.rgb, LUMINANCE_VECTOR);
    result.rgb += (noise - 0.5) * ui_FilmGrainStrength * (0.5 + lum);
    return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Vignette Constants
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

static const float2 VIGNETTE_CENTER = float2(0.5, 0.5);

// Screen Vignette function
float4 applyVignette(VtoP IN, float4 result)
{
    float2 d = (IN.tex0.xy - VIGNETTE_CENTER) / max(ui_VignetteRadius, 1e-3);
    float r2 = dot(d,d);

    // smooth falloff – no hard ring, only darken edges
    float v = exp(-ui_VignetteCurve * r2);
    float f = lerp(1.0, v, ui_VignetteAmount);

    result.rgb *= f;
    return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// God Rays (Screen-Space Volumetric Light Scattering)
// Based on Kenny Mitchell's GPU Gems 3 approach.
// Sun screen position is projected CPU-side by the shaderloader using
// the sky shader's ViewProj matrix.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float3 computeGodRays(float2 uv)
{
    // Sun behind camera or not available
    if (cvSunScreenPos.z < 0.5) return 0;

    // Sun below horizon: in Carbon's coordinate system, negative Y = above horizon.
    // Fade out smoothly as sun drops below horizon (Y approaches positive).
    float horizonFade = saturate(-cvSunDirection.y * 5.0 + 0.5);
    if (horizonFade < 0.01) return 0;

    float2 sunUV = cvSunScreenPos.xy;

    // Skip if sun is far off-screen (margin for rays to reach in from edges)
    if (sunUV.x < -0.5 || sunUV.x > 1.5 || sunUV.y < -0.5 || sunUV.y > 1.5)
        return 0;

    // Vector from pixel toward sun
    float2 rayDir = sunUV - uv;
    float rayLength = length(rayDir);

    // Angular falloff: squared for more natural radial gradient
    float angularFade = saturate(1.0 - rayLength * 0.6);
    angularFade *= angularFade;

    // Screen-edge fade: prevent hard cutoff at screen borders
    float2 edgeDist = min(uv, 1.0 - uv);
    float screenEdgeFade = saturate(min(edgeDist.x, edgeDist.y) * 15.0);

    // Step size
    static const int NUM_SAMPLES = 32;
    float2 deltaUV = rayDir / float(NUM_SAMPLES) * ui_GodRayDensity;

    // Per-pixel jitter: offset the starting position to break banding.
    // Hash22 gives stable noise that doesn't flicker between frames.
    float jitter = Hash22(uv * 1920.0).x;
    float2 currentUV = uv + deltaUV * jitter;

    float illuminationDecay = 1.0;
    float3 accumLight = 0;

    [loop]
    for (int i = 0; i < NUM_SAMPLES; i++)
    {
        currentUV += deltaUV;
        float2 sampleCoord = saturate(currentUV);

        // Soft depth threshold: smoothstep avoids hard sky/geometry boundary
        float d = readDepth24(sampleCoord);
        float lightMask = smoothstep(0.990, 0.999, d);

        // Sample actual sky color for natural tinting (sun=warm, moon=cool)
        float3 skyColor = tex2D(DIFFUSE_SAMPLER, sampleCoord).rgb;

        // Only use sky color where depth says it's sky, otherwise black
        accumLight += skyColor * lightMask * illuminationDecay * ui_GodRayWeight;
        illuminationDecay *= ui_GodRayDecay;
    }

    // Apply user tint on top of natural sky color, plus all fades
    float3 result = accumLight * ui_GodRayTint.rgb;
    result *= angularFade * screenEdgeFade * horizonFade;
    result *= (1.0 / float(NUM_SAMPLES));

    return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Shader Pass
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 ShaderPass(const VtoP IN, in float4 result)
{
    float3 bloomColor;

    // --- STAGE 1: SCENE REFERRED (Linear Space / Physics) ---

    // Auto-Exposure: scale the raw scene-referred signal before any grading.
    // Applied here, before tonemapping, so the full pipeline sees the
    // corrected exposure -- not just bloom or a single post-process step.
    result.rgb *= ComputeAutoExposure();

    // Whitebalance
    if (ui_UseWhitebalance > 0.5) result.rgb = adaptiveWhiteBalance(result.rgb);

    // God Rays (volumetric light scattering from sun/moon)
    if (ui_GodRayStrength > 0.01)
    {
        float3 rays = computeGodRays(IN.tex0.xy);
        result.rgb += rays * ui_GodRayStrength;
    }

    // --- STAGE 2: TONEMAPPING (The Camera Sensor) ---

    // Tonemap
    result.rgb = ApplyTonemap(result.rgb, (int)ui_TonemapSelector);

    // --- STAGE 3: DISPLAY REFERRED (Styling / Grading) ---

    // Color Temperature
    result.rgb = adjustTemperature(result.rgb, ui_ColorTemp);

    // Desaturation
    result = applyDesaturation(result, ui_DesaturationAmount);

    // Legacy Curves
    if (ui_useLegacyCurve > 0.5) result.rgb = LegacyCurveTonemap(result.rgb);

    // Color Processing
    result.rgb = processColor(result.rgb);

    // --- STAGE 4: OPTICAL & LENS DEFECTS (Post-Process) ---

    // Bloom
    if (ui_BloomIntensity > 0.01)
    {
        float4 bloomEffect = tex2D(BLOOM_SAMPLER, IN.tex0.xy);
        bloomColor = ApplyBloomThresholdSoftKnee(bloomEffect.rgb * ui_BloomIntensity);
        result.rgb = lerp(1.0f - (1.0f - result.rgb) * (1.0f - bloomColor), result.rgb + bloomColor, BLOOM_BLEND_MODE);
    }

    // LensDirt (only compute when enabled)
    if (ui_LensDirtIntensity > 0.01)
    {
        float4 dirtRes = applyLensDirt(IN, result);
        result = dirtRes;
    }

    // FilmGrain (only compute when enabled)
    if (ui_FilmGrainStrength > 0.001)
    {
        result = applyFilmGrain(IN, result);
    }

    // Vignette (only compute when enabled)
    if (ui_VignetteAmount > 0.01)
    {
        result = applyVignette(IN, result);
    }

    return result;

}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// you shouldnt touch these
// Visualtreatment Function
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 PS_VisualTreatment(const VtoP IN, uniform bool doDepthOfField, uniform bool doColourFade) : COLOR
{
    // Predefenitions
    float depth = readDepth24(IN.tex0.xy);
    float4 result = tex2D(DIFFUSE_SAMPLER, IN.tex0.xy);

    // Chromatic Aberration: 0=Off, 1=NOS/Speed only, 2=Always
    if (ui_AberrationMode > 1.5)
    {
        // Always On: use fixed radius
        result = applyChromaticAberration(IN, result, ui_AberrationRadius);
    }
    else if (ui_AberrationMode > 0.5)
    {
        // NOS / Speed only: scale with sense-of-speed (2x boost for visibility)
        float nosScale = abs(cvBlurParams.x) / 7.0 * 2.0;
        if (nosScale > 0.001)
            result = applyChromaticAberration(IN, result, nosScale);
    }

    // Guard: skip the 4x tex2D fetches entirely when sharpen is off.
    if (ui_SharpenAmount > 0.01)
    {
        float4 sharpRes = applySharpen(IN, result);
        result = lerp(result, sharpRes, ui_SharpenAmount);
    }

#ifndef DONTDODEPTH
    if (doDepthOfField && cbDepthOfFieldEnabled)
    {
        result = DoDepthOfField(IN, result, depth);
    }
#endif

    // HDR
    if (ui_UseHDR > 0.5)
    {
        float4 hdr = applyHDR(IN, result);
        result.rgb = hdr.rgb;
    }

    if (ui_UsePostEffects > 0.5) result = ShaderPass(IN, result);

    // LUT Color Grading
    if (ui_UseLUT > 0.5)
    {
        result.rgb = saturate(result.rgb);
        result.rgb = tex3D(VOLUMEMAP_SAMPLER, result.rgb).rgb;
    }

#if USE_CUSTOM_COP_ENGANGEMENT == 1
    // Cop Intro Effect (Conditional Blending)
    float3 copTreatment = tex3D(BLENDVOLUMEMAP_SAMPLER, result.xyz).xyz;
    if (COP_INTRO_SCALE > 0.25)
    {
        // Apply color blend only if COP_INTRO_SCALE is non-zero
        float3 copBlended = lerp(copTreatment, INTRO_BLEND_COLOR, INTRO_BLEND_AMOUNT); // Blending copTreatment with color
        result.xyz = lerp(result.xyz, copBlended, COP_INTRO_SCALE);                    // Applying cop intro effect
    }
#endif

    if (ui_UseCustomSpeedbreaker > 0.5)
    {
        // Custom Speedbreaker (only when engine signals breaker is active)
        if (BREAKER_INTENSITY > 0.01)
        {
            result.rgb = saturate(result.rgb);

            float3 blendColor = lerp(result.xyz, ui_SpeedbreakerColor.rgb, ui_SpeedbreakerBlend);
            float3 goldenHighlight = float3(1.1, 1.0, 0.9);

            float w = saturate(max(blendColor.r, max(blendColor.g, blendColor.b)) * BREAKER_INTENSITY);

            float lum = dot(result.xyz, LUMINANCE_VECTOR);

            result.xyz = lerp(result.xyz, lum.xxx * 1.5, w);
            result.xyz *= lerp(1.0, goldenHighlight, BREAKER_INTENSITY);
            result.xyz = saturate(result.xyz);
        }
    }
    else
    {
        // Vanilla Speedbreaker
        float4 vignette = tex2D(MISCMAP2_SAMPLER, IN.tex0.xy);
        float lumBreaker = dot(result.xyz, LUMINANCE_VECTOR);
        result.xyz = lerp(result.xyz, lumBreaker * 1.5, saturate(vignette.y) * BREAKER_INTENSITY);
    }

    // Apply Brightness adjustments
    result.rgb *= ui_Brightness;

    // NIS fade
    if (doColourFade)
    {
        result.xyz = lerp(result.xyz, cvVisualEffectFadeColour.xyz, cvVisualEffectFadeColour.w);
    }

    return result;
}
#endif

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Cliff-fall Function
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 PS_UvesOverCliff(const VtoP IN, uniform bool doColourFade) : COLOR
{
    float4 result;

    // Initialize result with screen texture
    float4 screenTex = tex2D(DIFFUSE_SAMPLER, IN.tex0.xy);
    result = screenTex;

    // Vignette (single pass – using custom applyVignette only)
    float4 vignetteRes = applyVignette(IN, result);
    result = lerp(result, vignetteRes, step(0.01, ui_VignetteAmount));

    // LUT Color Grading
    if (ui_UseLUT > 0.5)
    {
        result.rgb = saturate(result.rgb);
        result.rgb = tex3D(VOLUMEMAP_SAMPLER, result.rgb).rgb;
    }

    // Engine vignette scale applied as edge darkening (replaces MISCMAP2 double-vignette)
    float2 fromEdge = abs(IN.tex0.xy - 0.5) * 2.0;
    float edgeFactor = max(fromEdge.x, fromEdge.y);
    float edgeDarken = lerp(1.0, 1.0 - VIGNETTE_SCALE, smoothstep(0.5, 1.0, edgeFactor));
    result.rgb *= edgeDarken;

    // Calculate max channel for cliff effect
    float maxChannel = max(max(result.x, result.y), result.z);

#if USE_CUSTOM_CLIFF_EFFECT == 1
    // Blend/fade to color for cliff effect
    float3 cliffBlendColor = CLIFF_COLOR; // example RGB color for the fade
    float cliffBlendAmount = saturate(maxChannel * CLIFF_BLEND); // control blend amount dynamically
    result.xyz = lerp(result.xyz, cliffBlendColor, cliffBlendAmount);
#endif

    // NIS fade
    if (doColourFade)
    {
        result.xyz = lerp(result.xyz, cvVisualEffectFadeColour.xyz, cvVisualEffectFadeColour.w);
    }

    return result;
}